<!--
 * Copyright © 2022
 * @Author: zhangxiang
 * @Date: 2022-08-04 11:13:39
 * @LastEditors: zhangxiang
 * @LastEditTime: 2023-07-24 09:41:22
 * @Description: 文件分片上传，步骤：上传初始化->分片并上传->合并，同时可查询进度
 *
-->

<template>
  <div class="file-path-uploader" style="display: none">
    <input
      ref="inputFile"
      style="display: none"
      :type="inputType"
      :accept="accept"
      @change="fileInputChangeFile"
    />
  </div>
</template>
<script>
// import BMF from 'browser-md5-file'
import {
  initUploadApp,
  uploadFilePatch,
  mergeFilePatch,
  getFileProgress
} from '@/api/cnap/app-service'
import { formatSize } from '@/utils/index'

const SIZE_MB = 1024 * 1024 // MB
const PROGRESS_INTERVAL = 1000 // 获取进度时间间隔

const STAGES_UPLOAD = ['patch', 'merge']

export default {
  name: 'FilePatchUploader',
  props: {
    limitSize: {
      // 大小限制
      type: Number,
      default: 0 // 不限
    },
    accept: {
      // 可接受文件类型
      type: String,
      default: '*'
    },
    patchSize: {
      // 分片大小
      type: Number,
      default: 10 // 分片大小，单位：M，默认10M
    }
  },
  data() {
    return {
      inputType: 'file', // input类型
      file: null, // 文件
      patchSizeByte: this.patchSize * SIZE_MB, // 分片大小
      timerProgress: null, // 获取进度timer
      appInfo: {}, // 应用信息
      isInMerge: false, // 是否在合中并
      hasMergePatchs: false, // 是否已经和
      averageSpeed: 0, // 平均速度
      currentSpeed: 0, // 当前速度
      firstUploadedTime: 0, // 初次上传时间
      prevUploadedSize: 0, // 上次上传速度计算大小
      lastUploadedTime: 0, // 上次上传速度计算时间
      stage: 'patch', // 上传流程 patch-上传 merge-合并 unzip-分解 push-推送
      percentage: 0, // 上传百分比
      maxCount: 5, // 最大同时上传文件块的进程数
      currentCount: 0, // 当前同时上传文件块的进程数
      taskQueue: [] // 等待执行上传的任务队列
    }
  },
  computed: {
    /**
     * @description: 前端上传速度信息
     * @return {*}
     */
    infoChange() {
      const { percentage, averageSpeed } = this
      return { percentage, averageSpeed }
    }
  },
  watch: {
    /**
     * @description: 监听上传速度
     * @param {*} newVal
     * @return {*}
     */
    infoChange(newVal) {
      // 抛出进度事件
      this.$emit('progress', {
        name: this.file.name, // 文件名称
        size: formatSize(this.file.size), // 文件大小
        progress: this.percentage, // 上传比例
        speed:
          this.stage === 'patch' ? formatSize(this.averageSpeed) + ' / s' : '', // 上传速度
        text:
          this.stage === 'patch'
            ? '文件上传中...'
            : this.stage === 'merge'
              ? '文件合并中...'
              : '' // 相关提示
      })
    }
  },
  methods: {
    /**
     * @description: 停止上传
     * @return {*}
     */
    stopUploadFile() {
      if (this.timerProgress) {
        clearTimeout(this.timerProgress)
        this.timerProgress = null
      }
      this.initSetting() // 初始化配置
    },
    /**
     * @description: 错误回调
     * @param {*} value
     * @return {*}
     */
    errorCallback(value) {
      if (this.timerProgress) {
        clearTimeout(this.timerProgress)
        this.timerProgress = null
      }
      this.$emit('error', value) // 抛出异常事件
    },
    /**
     * @description: 合并文件分片
     * @param {*} parentFileMd5
     * @param {*} parentFileId
     * @param {*} totalSize
     * @return {*}
     */
    mergePatchs(parentFileMd5, parentFileId, totalSize) {
      this.isInMerge = true
      // 合并文件请求
      mergeFilePatch({
        parentId: parentFileId,
        md5: parentFileMd5,
        totalSize: totalSize,
        appInfo: this.appInfo
      })
        .then((res) => {
          console.timeEnd('patchUpload')
          // 修改状态位
          this.hasMergePatchs = true
          this.isInMerge = false
        })
        .catch((err) => {
          console.log(err)
          this.isInMerge = false
        })
    },
    /**
     * @description: 获取上传进度及状态
     * @param {*} parentFileMd5
     * @param {*} parentFileId
     * @param {*} patchCount
     * @return {*}
     */
    getUploadResult(parentFileMd5, parentFileId, patchCount) {
      // 获取后端文件上传进度
      getFileProgress(parentFileId)
        .then((res) => {
          if (res.data) {
            this.percentage = Math.min(100, Math.round(res.data.percent)) // 进度不超过100
            this.stage = res.data.stage || 'patch'
            if (
              Array.isArray(res.data.patchStatus) &&
              res.data.patchStatus.length > 0 &&
              res.data.patchStatus.length === patchCount
            ) {
              // 后端接收分片符合前端上传长度
              // 全部分片上传完成
              const uploadPatchsSuccess = res.data.patchStatus.every((item) => {
                return item.status === 1 // 上传完成
              })
              if (
                !this.hasMergePatchs &&
                uploadPatchsSuccess &&
                !this.isInMerge
              ) {
                // 未发送过合并文件请求 且 全部分片上传完成 且 没有正在merge请求中
                this.mergePatchs(parentFileMd5, parentFileId, this.file.size)
              }
              // 上传结束
              const uploadPatchsEnd = res.data.patchStatus.every((item) => {
                return item.status === 1 || item.status === 2 // 上传完成或上传失败
              })
              if (uploadPatchsEnd && !uploadPatchsSuccess) {
                // 存在上传失败的分片
                // 找出上传失败的分片
                const errPatchs = res.data.patchStatus.filter((item) => {
                  return item.status === 2 // 上传失败
                })
                for (const item of errPatchs) {
                  // 重新上传
                  this.uploadSingePatch(
                    parentFileMd5,
                    parentFileId,
                    item.index
                  )
                }
              }
            }
            if (
              Array.isArray(res.data.patchStatus) &&
              res.data.patchStatus.length > 0
            ) {
              // 统计上传完成的分片
              const successPatchs = res.data.patchStatus.filter((item) => {
                return item.status === 1 // 上传完成
              })
              // 计算速度
              this.measureSpeed(successPatchs.length)
            }
          }
          if (STAGES_UPLOAD.includes(this.stage) && this.percentage < 100) {
            // 文件没有上传完成
            this.timerProgress = setTimeout(() => {
              // 递归获取上传结果
              this.getUploadResult(parentFileMd5, parentFileId, patchCount)
            }, PROGRESS_INTERVAL)
          } else {
            // 上传完成
            if (this.timerProgress) {
              clearTimeout(this.timerProgress)
              this.timerProgress = null
            }
            console.timeEnd('allUpload')
            this.$emit('success') // 抛出事件
          }
        })
        .catch((err) => {
          this.errorCallback({
            title: '文件上传失败',
            detail: err
          }) // 异常处理
        })
    },
    /**
     * @description: 计算上传速度
     * @param {*} length
     * @return {*}
     */
    measureSpeed(length) {
      const uploaded = this.patchSizeByte * length // 已上传
      const currentTime = Date.now()
      const timeSpan = currentTime - this.lastUploadedTime // 时间间隔
      this.currentSpeed = Math.max(
        ((uploaded - this.prevUploadedSize) / timeSpan) * 1000,
        0
      ) // 当前速度
      this.prevUploadedSize = uploaded
      this.lastUploadedTime = currentTime
      const timeSpanTotal = currentTime - this.firstUploadedTime // 目前总耗时
      const aveSpeed = Math.max((uploaded / timeSpanTotal) * 1000, 0) // 平均速度
      this.averageSpeed = aveSpeed * 0.9 + this.currentSpeed * 0.1 // 加权平均速度
    },
    /**
     * @description: 上传单个分片
     * @param {*} parentFileMd5
     * @param {*} parentFileId
     * @param {*} patchIndex
     * @return {*}
     */
    uploadSingePatch(parentFileMd5, parentFileId, patchIndex) {
      var start = patchIndex * this.patchSizeByte // 分片在文件中开始位置
      var end = Math.min(this.file.size, start + this.patchSizeByte) // 分片在文件中结束位置
      var patchFile = this.file.slice(start, end) // 分片
      const params = new FormData() // 构造数据
      params.append('parentId', parentFileId)
      params.append('md5', parentFileMd5)
      params.append('index', patchIndex)
      params.append('fileName', this.file.name)
      params.append('patchSize', end - start)
      params.append('patch', patchFile)
      // uploadFilePatch(params)
      const uploaderTask = this.createTask(uploadFilePatch, params) // 创建上传任务
      if (this.currentCount >= this.maxCount) {
        // 当前在上传的分配任务数超出允许的最大值
        this.taskQueue.push(uploaderTask) // 加入队列
      } else {
        uploaderTask() // 启动任务
      }
    },
    /**
     * @description: 创建任务
     * @param {*} caller
     * @param {*} params
     * @return {*}
     */
    createTask(caller, params) {
      return () => {
        caller(params)
          .catch(() => {
            // 异常重试
            this.uploadSingePatch(
              params.get('md5'),
              params.get('parentId'),
              params.get('index')
            )
          })
          .finally(() => {
            // 任务完成处理
            this.currentCount-- // 当前任务数减1
            if (this.taskQueue.length > 0) {
              const task = this.taskQueue.shift() // 从队列中取出任务
              task() // 执行任务
            }
          })
        this.currentCount++ // 当前任务数加1
      }
    },
    /**
     * @description: 上传全部分片
     * @param {*} parentFileMd5
     * @param {*} parentFileId
     * @param {*} patchCount
     * @return {*}
     */
    uploadPatchs(parentFileMd5, parentFileId, patchCount) {
      for (let i = 0; i < patchCount; i++) {
        this.uploadSingePatch(parentFileMd5, parentFileId, i)
      }
    },
    /**
     * @description: 初始化分片上传
     * @param {*} parentFileMd5
     * @param {*} extraData
     * @return {*}
     */
    initPatchUpload(parentFileMd5, extraData) {
      console.time('patchUpload')
      const patchCount = Math.ceil(this.file.size / this.patchSizeByte) // 分片数
      const fileInfo = {
        name: this.file.name,
        md5: parentFileMd5,
        totalSize: this.file.size,
        patchNums: patchCount
      } // 文件信息
      this.appInfo = {
        ...extraData,
        fileInfo
      } // 应用信息
      // 初始化上传
      initUploadApp(this.appInfo)
        .then((res) => {
          const parentFileId = res.data
          // 上传分片
          this.uploadPatchs(parentFileMd5, parentFileId, patchCount)
          // 获取结果
          this.getUploadResult(parentFileMd5, parentFileId, patchCount)
        })
        .catch((err) => {
          this.errorCallback({
            title: '初始化文件上传失败',
            detail: err
          }) // 异常处理
        })
    },
    /**
     * @description: 开始上传
     * @param {*} extraData
     * @return {*}
     */
    startUploadFile(extraData) {
      console.time('allUpload')
      this.$emit('start')
      this.initSetting()
      this.initPatchUpload('', extraData)
      // 获取文件md5值
      // console.time('md5')
      // const bmf = new BMF()
      // bmf.md5(
      //   this.file,
      //   (err, md5) => {
      //     if (!err) {
      //       console.timeEnd('md5')
      //       this.initPatchUpload(md5, extraData)
      //     } else {
      //       this.errorCallback({
      //         title: '获取文件md5失败',
      //         detail: err
      //       })
      //     }
      //   },
      //   (progress) => {
      //     this.percentage = Math.round(progress * 100 * 0.2)
      //   }
      // )
    },
    /**
     * @description: 初始化配置
     * @return {*}
     */
    initSetting() {
      this.isInMerge = false
      this.prevUploadedSize = 0
      this.lastUploadedTime = Date.now()
      this.firstUploadedTime = Date.now()
      this.hasMergePatchs = false
      this.percentage = 0
      this.currentCount = 0
      this.taskQueue = []
    },
    /**
     * @description: 取消选择
     * @return {*}
     */
    cancelSelect() {
      this.inputClear()
      this.file = null
    },
    /**
     * @description: 选择文件
     * @return {*}
     */
    triggerSelect() {
      this.$refs.inputFile.click()
    },
    /**
     * @description: 文件选择回调
     * @param {*} e
     * @return {*}
     */
    fileInputChangeFile(e) {
      const files = e.target.files
      if (files && files.length > 0) {
        if (
          this.limitSize <= 0 ||
          files[0].size / 1024 / 1024 < this.limitSize
        ) {
          this.$emit('selected', files[0]) // 抛出事件
          this.file = files[0]
        } else {
          this.$message.warning('文件大小超出限制')
        }
      } else {
        this.$message.warning('选择文件出错，请重新选择')
      }
      this.inputClear()
    },
    /**
     * @description: 清除选择格式
     * @return {*}
     */
    inputClear() {
      this.inputType = 'text'
      setTimeout(() => {
        this.inputType = 'file'
      }, 100)
    }
  }
}
</script>
